// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
//
-// Copyright 2024 GNOME Foundation, Inc.
+// Copyright 2024, 2025 GNOME Foundation, Inc.
//
// This is a GNOME Shell component to support screen time limits and statistics.
//
*
* Active/Inactive time is based off the total time the user account has spent
* logged in to at least one active session, not idle (and not locked, but
- * that’s a subset of idle time).
+ * that’s a subset of idle time), and not suspended.
* This corresponds to the `active` state from sd_uid_get_state()
* (https://www.freedesktop.org/software/systemd/man/latest/sd_uid_get_state.html),
* plus `IdleHint` from
- * https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.login1.html#User%20Objects.
+ * https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.login1.html#User%20Objects,
+ * plus `PreparingForSleep` from
+ *.https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.login1.html#The%20Manager%20Object.
* ‘Inactive’ time corresponds to all the other states from sd_uid_get_state(),
- * or if `IdleHint` is true.
+ * or if `IdleHint` is true or if `PreparingForSleep` is true.
*
* All times within the class are handled in terms of wall/real clock time,
* rather than monotonic time. This is because it’s necessary to continue
'daily-limit-reached': {},
},
}, class TimeLimitsManager extends GObject.Object {
- constructor(historyFile, clock, loginUserFactory, settingsFactory) {
+ constructor(historyFile, clock, loginManagerFactory, loginUserFactory, settingsFactory) {
super();
// Allow these few bits of global state to be overridden for unit testing
return timeChangeSource.attach(null);
},
};
+ this._loginManagerFactory = loginManagerFactory ?? {
+ new: LoginManager.getLoginManager,
+ };
this._loginUserFactory = loginUserFactory ?? {
newAsync: () => {
const loginManager = LoginManager.getLoginManager();
this._state = TimeLimitsState.DISABLED;
this._stateTransitions = [];
this._cancellable = null;
+ this._loginManager = null;
this._loginUser = null;
this._lastStateChangeTimeSecs = 0;
this._timerId = 0;
}
// Start listening for notifications to the user’s state.
+ this._loginManager = this._loginManagerFactory.new();
+ this._loginManager.connectObject(
+ 'prepare-for-sleep',
+ () => this._updateUserState(true).catch(
+ e => console.warn(`Failed to update user state: ${e.message}`)),
+ this);
+
this._loginUser = await this._loginUserFactory.newAsync();
this._loginUser.connectObject(
'g-properties-changed',
this._loginUser?.disconnectObject(this);
this._loginUser = null;
+ this._loginManager?.disconnectObject(this);
+ this._loginManager = null;
+
this._state = TimeLimitsState.DISABLED;
this._lastStateChangeTimeSecs = 0;
this.notify('state');
}
_calculateUserStateFromLogind() {
- const isActive = this._loginUser.State === 'active' && !this._loginUser.IdleHint;
+ const isActive = this._loginUser.State === 'active' &&
+ !this._loginUser.IdleHint &&
+ !this._loginManager.preparingForSleep;
return isActive ? UserState.ACTIVE : UserState.INACTIVE;
}
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
//
-// Copyright 2024 GNOME Foundation, Inc.
+// Copyright 2024, 2025 GNOME Foundation, Inc.
//
// This is a GNOME Shell component to support screen time limits and statistics.
//
this._loginUserPropertiesChangedCallback = null;
+ this._currentPreparingForSleepState = false;
+ this._loginManagerPrepareForSleepCallback = null;
+
// Create a fake history file containing the given contents. Or, if no
// contents are given, reserve a distinct new history file name but then
// delete it so it doesn’t exist for the manager.
// And a mock D-Bus proxy for logind.
const harness = this;
+
+ class MockLoginManager {
+ connectObject(signalName, callback, unusedObject) {
+ if (signalName === 'prepare-for-sleep') {
+ if (harness._loginManagerPrepareForSleepCallback !== null)
+ fail('Duplicate prepare-for-sleep connection');
+ harness._loginManagerPrepareForSleepCallback = callback;
+ } else {
+ // No-op for mock purposes
+ }
+ }
+
+ disconnectObject(unused) {
+ // Very simple implementation for mock purposes
+ harness._loginManagerPrepareForSleepCallback = null;
+ }
+
+ get preparingForSleep() {
+ return harness._currentPreparingForSleepState;
+ }
+ }
+
class MockLoginUser {
connectObject(signalName, callback, unusedObject) {
if (signalName === 'g-properties-changed') {
}
}
+ this._mockLoginManager = new MockLoginManager();
this._mockLoginUser = new MockLoginUser();
}
});
}
+ /**
+ * Add a pair of sleep and resume events to the event queue. This simulates
+ * the machine being asleep (suspended) and then resumed after a period of
+ * time. No other events should be inserted into the queue between these
+ * two.
+ *
+ * This simulates the [D-Bus API for logind](https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.login1.html#The%20Manager%20Object)
+ * notifying to prepare for sleep at date/time `timeStr`, then sleeping,
+ * then notifying that the machine has resumed from sleep at date/time
+ * `timeStr` plus `duration` (in seconds).
+ *
+ * @param {string} timeStr Date/Time the prepare-for-sleep event happens,
+ * in ISO 8601 format.
+ * @param {number} duration Duration of the sleep/suspend period, in seconds
+ */
+ addSleepAndResumeEvent(timeStr, duration) {
+ this._insertEvent({
+ type: 'preparing-for-sleep-state-change',
+ time: TestHarness.timeStrToSecs(timeStr),
+ newPreparingForSleepState: true,
+ });
+ this._insertEvent({
+ type: 'preparing-for-sleep-state-change',
+ time: TestHarness.timeStrToSecs(timeStr) + duration,
+ newPreparingForSleepState: false,
+ });
+ }
+
/**
* Add a login user state change event to the event queue. This simulates
* the [D-Bus API for logind](https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.login1.html#User%20Objects)
this._currentTimeSecs = TestHarness.timeStrToSecs(timeStr);
}
+ /**
+ * Get a mock login manager factory for use in the `TimeLimitsManager` under
+ * test. This is an object providing a constructor for the objects returned
+ * by `LoginManager.getLoginManager()`. This is roughly a wrapper around the
+ * [`org.freedesktop.login1.Manager` D-Bus API](https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.login1.html#The%20Manager%20Object).
+ * Each constructor returns a basic implementation of the manager which uses
+ * the current state from `TestHarness`.
+ *
+ * This has an extra layer of indirection to match `mockSettingsFactory`.
+ */
+ get mockLoginManagerFactory() {
+ return {
+ new: () => {
+ return this._mockLoginManager;
+ },
+ };
+ }
+
/**
* Get a mock login user factory for use in the `TimeLimitsManager` under
* test. This is an object providing constructors for `LoginUser` objects,
if (this._timeChangeNotify)
this._timeChangeNotify();
break;
+ case 'preparing-for-sleep-state-change':
+ this._currentPreparingForSleepState = event.newPreparingForSleepState;
+
+ if (this._loginManagerPrepareForSleepCallback)
+ this._loginManagerPrepareForSleepCallback(this._currentPreparingForSleepState);
+ break;
case 'login-user-state-change':
this._currentUserState = event.newUserState;
this._currentUserIdleHint = event.newUserIdleHint;
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.DISABLED);
harness.expectState('2024-06-01T15:00:00Z', timeLimitsManager, TimeLimitsState.DISABLED);
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.addSettingsChangeEvent('2024-06-01T11:00:00Z',
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.expectProperties('2024-06-01T13:59:59Z', timeLimitsManager, {
},
});
harness.initializeMockClock('2024-06-01T00:30:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
harness.expectState('2024-06-01T00:30:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.expectProperties('2024-06-01T01:29:59Z', timeLimitsManager, {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.expectProperties('2024-06-01T13:59:59Z', timeLimitsManager, {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.expectProperties('2024-06-01T15:00:00Z', timeLimitsManager, {
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
// Run until the limit is reached.
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
// The existing history file (above) lists two active periods,
// 07:30–08:00 and 08:30–09:30 that morning. So the user should have
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
// The existing history file (above) lists one active period,
// 04:30–08:50 that morning. So the user should have no time left today.
},
}, invalidHistoryFileContents);
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
// The existing history file (above) is invalid or a no-op and
// should be ignored.
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
// The existing history file (above) lists two active periods,
// one of which is a long time ago and the other is ‘this’ morning in
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
// The existing history file (above) lists one active period,
// 04:30–08:50 that morning IN THE YEAR 3000. This could have resulted
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.addSettingsChangeEvent('2024-06-01T10:00:02Z',
},
]));
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.expectState('2024-06-01T14:00:01Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
// Use up 2h of the daily limit.
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
},
});
harness.initializeMockClock('2024-06-01T10:00:00Z');
- const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
// Use up 2h of the daily limit.
harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
harness.run();
});
+
+ it('doesn’t count usage when asleep/suspended', () => {
+ const harness = new TestHarness({
+ 'org.gnome.desktop.screen-time-limits': {
+ 'history-enabled': true,
+ 'daily-limit-enabled': true,
+ 'daily-limit-seconds': 4 * 60 * 60,
+ },
+ });
+ harness.initializeMockClock('2024-06-01T10:00:00Z');
+ const timeLimitsManager = new TimeLimitsManager.TimeLimitsManager(harness.mockHistoryFile, harness.mockClock, harness.mockLoginManagerFactory, harness.mockLoginUserFactory, harness.mockSettingsFactory);
+
+ // Use up 2h of the daily limit.
+ harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectProperties('2024-06-01T12:00:00Z', timeLimitsManager, {
+ 'state': TimeLimitsState.ACTIVE,
+ 'dailyLimitTime': TestHarness.timeStrToSecs('2024-06-01T14:00:00Z'),
+ });
+
+ // Sleep for 3h and that shouldn’t use up limit time.
+ harness.addSleepAndResumeEvent('2024-06-01T12:00:01Z', 3 * 60 * 60);
+ harness.expectProperties('2024-06-01T15:00:02Z', timeLimitsManager, {
+ 'state': TimeLimitsState.ACTIVE,
+ 'dailyLimitTime': TestHarness.timeStrToSecs('2024-06-01T17:00:00Z'),
+ });
+
+ harness.shutdownManager('2024-06-01T15:20:00Z', timeLimitsManager);
+
+ harness.run();
+ });
});